blocky 全 LAN NXDOMAIN 事故与 DNS 分流修复

🌿 Evergreen source dns blocky mihomo k8s talos

blocky 全 LAN NXDOMAIN 事故与 DNS 分流修复

来源信息

  • 标题:blocky DNS 分流排查与修复记录(2026-09-22)
  • 作者:
  • 链接:<local-project-path>
  • 时间:2026-09-22
  • 环境:Talos v1.11.5 / k8s v1.34.0 单节点集群,kubeconfig context admin@main;配置仓库 ~/clustertool/(非 git 仓库,用文件 + 时间戳备份管理)

一句话摘要

一次 helm upgrade 后全 LAN 域名大面积 NXDOMAIN:新增的客户端分组只配了 allowlist、没配 denylist,命中 blocky v0.33.0「客户端任一分组是白名单组 ⇒ 整个客户端进入白名单模式」的语义缺陷。回滚后升级到 v0.35.0 恢复,并顺带修复了两处分流缺陷:*.cluster.local 在 blocky 侧无映射、自有域名回落被节点 TUN 劫持成 fake-ip 并被缓存下发到全 LAN。

脱敏说明

真实 IP、域名与主机名已替换为占位符:

占位符 含义
<router-ip> 家庭网关 / DHCP DNS 下发点
<main-dns-ip> blocky 的 MetalLB 地址(LAN 主 DNS,:53)
<mihomo-dns-ip> / <mihomo-service-ip> mihomo 的 MetalLB 地址 / 其 DNS 监听端口 1053
<talos-node-ip> 单节点 Talos 主机
<ingress-ip> 内网 nginx ingress(MetalLB)
<dashboard-ip> / <other-svc-ip> 其他自建服务的 LB 地址
<kube-dns-ip> kube-dns ClusterIP
<k8s-api-vip> kubernetes.default ClusterIP
<blocky-clusterip> / <k8s-gateway-clusterip> 对应 Service 的 ClusterIP
<pod-cidr> / <service-cidr> / <lan-cidr> 集群 Pod 网段 / Service 网段 / 局域网段
<home-domain> 自有域名(k8s_gateway 管理的 zone)
<corp-domain> 需要放行的内部业务域名

结论摘要

# 问题 根因 处置
事故 全 LAN 域名大面积 NXDOMAIN,<home-domain>、docker.io、registry.k8s.io、iterm2.com、容器镜像拉取全部失败 rev14 给 custom 组只配了 allowlist、没配 denylist,而 blocky v0.33.0 对「客户端任一分组是白名单组」即进入全局 allowlist-only(只有 chart 内置 3 份白名单约 5.6 万条域名能解析) 先 helm rollback 13 恢复,再升级到 blocky v0.35.0(上游 #2249 已修)
修复 1 *.cluster.local 在 blocky 侧无映射 → 打到公网上游 NXDOMAIN;mihomo 的 nameserver-policy 指向 blocky 成为死路 blocky 缺 cluster.local 映射;mihomo *.cluster.local 的 * 只匹配一级;mihomo 自身 DNS 客户端不可达非回环地址 blocky 加 cluster.local → kube-dns;mihomo 过滤器改 +.,策略目标改 127.0.0.53
修复 2 自有域名及其未命中 k8s 资源的子域返回 fake-ip 198.18.x(被 blocky 缓存后下发全 LAN,LAN 不可路由) k8s_gateway 的 fallthrough 落到明文 forward . 1.1.1.1,被节点 mihomo TUN 的 :53 劫持 打开 chart 提供的 k8sgateway.forward,回落改走 AliDNS DoT(853)(不被劫持)

最终状态:release rev18(chart blocky-22.25.0 / blocky v0.35.0),blocky 2/2、blocky-redis 1/1、mihomo 1/1,values.yaml 与 release 零漂移。

分流拓扑

flowchart LR
  C["LAN 客户端 / Talos 节点 hostDNS"]
  R["路由器"]
  B["blocky :53<br/>chart blocky-22.25.0 / v0.35.0"]
  M["mihomo :1053<br/>fake-ip 198.18.0.0/16"]
  G["k8s_gateway 127.0.0.1:5353<br/>zone: 自有域名"]
  K["kube-dns :53"]
  U["223.5.5.5 / 114.114.114.114"]
  I["内网 nginx ingress"]
  C --> R --> B
  B -->|"≈7.2k 条 GFW + 自定义域名"| M
  B -->|"cluster.local"| K
  B -->|"自有域名"| G
  B -->|"默认"| U
  G -->|"未命中 k8s 资源 → DoT 853"| U
  G -->|"Ingress / Service / HTTPRoute"| I

blocky 解析顺序:blocking(拦截) → caching → conditional(分流) → upstreams(默认)。

各分叉明细(rev18 实测)

目标 条件 上游 说明
GFW / 自定义代理域名 conditional.mapping 中约 7.2k 条 <mihomo-dns-ip>:1053(mihomo) 返回 fake-ip 198.18.x,由 mihomo 按规则直连或代理
集群内部域名 conditional.mapping: cluster.local <kube-dns-ip>:53(kube-dns) svc.cluster.local / pod.cluster.local 均覆盖
集群内网服务入口 conditional.mapping: <home-domain>(chart 自动注入) 127.0.0.1:5353(k8s_gateway sidecar) 由 Ingress/Service/HTTPRoute 生成记录,指向 <ingress-ip>
其余域名 无匹配 223.5.5.5 / 114.114.114.114(timeout 1s) 真实地址
拦截 5 个 denylist 组(oisd、StevenBlack、adaway、Easyprivacy、AntiMalware…) blockType: nxDomain,TTL 6h 例外:allowlists.default(3 份白名单 URL)+ allowlists.custom(*.<corp-domain>)

其他关键参数:缓存 minTime 15m / 负缓存 30m,缓存落 redis(valkey,required: true);ports 53/853/4443/4000;HTTPS API 未暴露到 LAN(仅 ClusterIP 4000)。

k8s_gateway:Corefile zone .:5353,apex blocky.blocky,ttl 300,resources Ingress Service HTTPRoute,fallthrough(chart 硬编码,不可通过 values 关闭)。

mihomo:fake-ip-filter 与 nameserver-policy 见下文修复 1;tun.auto-route: true,route-exclude-address 含 <pod-cidr>、<service-cidr>、<lan-cidr>、DoT 出口 IP。

配置来源与部署流程

文件 作用
~/clustertool/myapp/blocky/values.yaml blocky 唯一真源(含 ≈7.2k 条 conditional.mapping、blocking、k8sgateway.forward)
~/clustertool/gfwlist/update-blocky-conditional.sh 拉取 3 个 GFW 源 + custom-domains.txt,重写 conditional.mapping(custom-domains.txt 中 domain|dnsserver 格式的条目会带自定义上游)
~/clustertool/gfwlist/{gfw,custom,whitelist}-domains.txt 域名清单;白名单用于剔除误杀(如某个白名单站点域名)
~/clustertool/mihomo/configmap.yaml mihomo 配置模板(__MIHOMO_SECRET__ 由 init 容器替换后写入 /data/config.yaml)
~/clustertool/clusters/main/kubernetes/core/blocky/ 已失效的 Flux GitOps 清单(chart 18.8.2,集群内没有 Flux CRD),不要按它操作

部署命令(README 记录的方式):

# 刷新域名列表(可选,会重写 values.yaml 的 mapping 段)
cd ~/clustertool/gfwlist && ./update-blocky-conditional.sh ../myapp/blocky/values.yaml custom-domains.txt whitelist-domains.txt

# 部署 blocky(chart 拉取经 mihomo 代理)
cd ~/clustertool && HTTPS_PROXY=http://<mihomo-service-ip>:7890 \
  helm upgrade blocky oci://oci.trueforge.org/truecharts/blocky -f myapp/blocky/values.yaml -n blocky

# 部署 mihomo
cd ~/clustertool && kubectl apply -f mihomo/configmap.yaml && kubectl -n network rollout restart deploy/mihomo

⚠️ blocky 不会可靠地热重载 ConfigMap:kubelet 用原子替换符号链接的方式更新挂载文件,blocky 的 fsnotify 不触发。改完 CM 必须重启 pod(kubectl -n blocky rollout restart deploy/blocky)。

事故:全 LAN 白名单模式(rev14)

时间线

时间(+08:00) 事件
07-26 23:25 rev13:chart 22.17.0 / blocky v0.33.0,一切正常
09-22 18:04:11 rev14:新增 blocking 块(custom 白名单组 + clientGroupsBlock.default 挂 custom)。该配置只存在于 release,磁盘上没有任何文件记录
18:50 有人刷新了 gfw-domains.txt(7021 → 7164 条),mapping 7029 → 7172
18:58 尝试升级 chart 22.25.0 失败:新 pod ImagePullBackOff(当时误判为镜像问题,实为 DNS)
19:04 按要求 helm rollback 13;因 redis STS pod 仍指向 rev15 的镜像摘要、blocky init 容器等 redis 而卡住 → 删除该 pod 后恢复
19:11 rev17:DNS 恢复后重跑升级 → 成功(chart 22.25.0 / v0.35.0)
19:19 rev18:修复 1、2

根因

custom 组只有 allowlist、没有同名 denylist,而 blocky v0.33.0 的判定是「客户端任一分组是白名单组 → 整个客户端进入白名单模式」:

// resolver/blocking_resolver.go  v0.33.0
func (r *BlockingResolver) hasAllowlistOnlyAllowed(groupsToCheck []string) bool {
    for _, group := range groupsToCheck {
        if _, found := r.allowlistOnlyGroups[group]; found { return true }   // 任一命中即 true
    }
    return false
}
func determineAllowlistOnlyGroups(cfg *config.Blocking) map[string]bool {
    // 只配了 allowlist、没有同名 denylist 的组 → 白名单组
}

默认客户端组包含 custom ⇒ 全 LAN(含经路由器转发的 Talos 节点)只在 chart 内置白名单(anudeepND ×2 + rahilpathan,共 55762 行)内可解析,其余一律 NXDOMAIN,query log 记为 BLOCKED (ALLOWLIST ONLY)。

上游已在 v0.35.0(commit 344de866,"scope allowlist-only mode to the whole client (#2249)", 2026-09-05)改为「客户端所有分组都是白名单组才进入白名单模式」;truecharts chart ≥ 22.23.1 才 pin v0.35.0。

触发原因

<host>.<corp-domain> 命中 chart 内置的 StevenBlack denylist,有人为放行它新增了白名单组 custom,并把 custom 挂进默认客户端组 —— 意图正确,语义踩坑。

影响面

  • 全 LAN + 集群节点(Talos hostDNS → <router-ip> → blocky)非白名单域名全部 NXDOMAIN;
  • LAN 上特意走 mihomo 的 registry(docker.io、quay.io、gcr.io、cgr.dev、factory.talos.dev、registry.k8s.io)、claude.ai、anthropic.com、iterm2.com 等全部不可解析;
  • 集群镜像拉取失败(kubelet 报 not found,其实是 DNS 解析失败;四个镜像摘要在 oci.trueforge.org / registry-1.docker.io / mirror.gcr.io 实测全部 HTTP 200,并非镜像缺失)。

当前(正确)的例外配置

blocking:
  clientGroupsBlock:
    - name: default
      groups: [default, default-ads, default-tracking, default-malicious, default-suspicious, custom]
  whitelist:
    - name: custom
      lists: ["*.<corp-domain>"]

在 blocky ≥ v0.35.0 下:客户端分组里既有带 denylist 的组、也有白名单组 ⇒ 白名单模式关闭,custom 的 allowlist 退化为「不受 denylist 影响的例外表」,正是预期语义。

修复 1:*.cluster.local 分流

三处协同改动(缺一不可):

位置 改动
myapp/blocky/values.yaml → conditional.mapping 新增 - domain: cluster.local / dnsserver: <kube-dns-ip>:53
gfwlist/custom-domains.txt 追加 cluster.local|<kube-dns-ip>:53(脚本支持的 domain|dnsserver 格式,避免下次刷新列表被覆盖)
mihomo/configmap.yaml → fake-ip-filter *.cluster.local、*.svc.cluster.local → +.cluster.local、+.svc.cluster.local(mihomo 的 * 只匹配一级,blocky.blocky.svc.cluster.local 会漏)
mihomo/configmap.yaml → nameserver-policy 5 个内网策略目标全部改为 127.0.0.53(Talos hostDNS)

为什么 mihomo 侧要指向回环

mihomo 自身 DNS 客户端在本机不可达任何非回环直连目标,实测(/dns/query 接口):

目标 结果
<main-dns-ip>(blocky LB) context deadline exceeded
tcp://<main-dns-ip>#eth0 超时
<blocky-clusterip>(blocky ClusterIP) 超时
<kube-dns-ip>(kube-dns ClusterIP) 超时
127.0.0.53(Talos hostDNS) ✅ 正常返回 <k8s-api-vip>
tls://1.1.1.1#Proxy(默认 nameserver) ✅ 正常(走代理出口)

排除项:tun.dns-hijack: [] 关掉后依旧超时(且会改变 TUN 侧 fake-ip 行为,已回退默认);同一 netns 里 kubectl exec 的 nslookup <main-dns-ip> 正常,说明是 mihomo 进程自身出口的问题。因此内网 DNS 策略必须经回环入口:127.0.0.53 → <router-ip> → blocky → kube-dns。

顺带修复(同一缺陷类)

另外 3 个策略目标(两个云厂商 API 域、一个镜像站域)同样指向不可达的 <main-dns-ip>,一并统一为 127.0.0.53;其中镜像站域在 fake-ip-filter 内(策略会被真正调用),修复前是真实 SERVFAIL。

修复 2:自有域名回落不再返回 fake-ip

chart 把 fallthrough 硬编码在 k8s_gateway 的 Corefile 里,唯一可用旋钮是 k8sgateway.forward:

k8sgateway:
  forward:
    enabled: true                 # 原 false(=明文 forward . 1.1.1.1)
    primary: tls://223.5.5.5      # chart 默认的 Cloudflare DoT 在本网络实测超时
    secondary: tls://223.6.6.6
    options:
      - name: tls_servername
        value: dns.alidns.com

原因链:forward.enabled: false → forward . 1.1.1.1(明文 :53)→ 被节点 mihomo TUN 的 :53 劫持 → 返回 fake-ip(自有域名曾是 198.18.0.37)→ 被 blocky 缓存(负缓存 30m / 正缓存 ≥15m)→ 下发全 LAN,而 198.18.0.0/16 在 LAN 不可路由。DoT(853) 不在劫持范围内,回落恢复真实解析。

实测验证矩阵(rev18 之后)

查询 blocky <main-dns-ip> mihomo <mihomo-dns-ip>:1053 判定
kubernetes.default.svc.cluster.local <k8s-api-vip> <k8s-api-vip> 修复 1 ✅
kube-dns.kube-system.svc.cluster.local <kube-dns-ip> — ✅
blocky.blocky.svc.cluster.local <blocky-clusterip> <blocky-clusterip> ✅
nope123.default.svc.cluster.local NXDOMAIN NXDOMAIN (Status 3) 不再超时 / fake-ip ✅
<home-domain> / <svc>.<home-domain> <ingress-ip> fake-ip(mihomo 自身设计) 修复 2 ✅
<home-domain>(直查 k8s_gateway <k8s-gateway-clusterip>:5353) <ingress-ip> — 原 198.18.0.37 ✅
docker.io / registry.k8s.io / ghcr.io 198.18.x 198.18.x 未受影响 ✅
openrouter.ai 104.18.2/3.115(真实) 198.18.x 非白名单域名正常 ✅
google.com 198.18.x 198.18.x ✅
ad-assets.futurecdn.net NXDOMAIN(拦截生效) fake-ip ✅
<host>.<corp-domain> 真实公网 IP(不再被 StevenBlack 误杀) — 例外表生效 ✅
经路由器 <router-ip>(LAN 全路径) 上述结果一致 — ✅

其他验证:k8s_gateway 直查五个 Ingress 主机均为 <ingress-ip>;mihomo 代理出口 curl -x http://<mihomo-service-ip>:7890 https://www.gstatic.com/generate_204 → 204;helm -n blocky get values 与 values.yaml 的 mapping 与 k8sgateway.forward 完全一致。

排查方法(可复用的证据链)

  1. 定位真源:brew services/ps/find ~ -iname "*blocky*" → ~/clustertool;确认 GitOps 目录是死配置(无 Flux CRD)。
  2. 三层比对:values.yaml(真源)↔ helm get values(release 用户值)↔ kubectl get cm blocky-config(渲染后的 config.yml)。本次正是靠 helm get values --revision N 的逐版本 diff 定位到 rev14 新增的 blocking 块。
  3. dig 矩阵定位范围:对照组(白名单内域名)能解析、目标域名 NXDOMAIN ⇒ 指向拦截而非链路问题。
  4. 读 query log 的 response_reason:kubectl -n blocky logs deploy/blocky -c blocky | grep question_name;BLOCKED (ALLOWLIST ONLY) 直接指向上游语义。
  5. 读对应版本的源码而非最新 main:raw.githubusercontent.com/0xERR0R/blocky/v0.33.0/resolver/blocking_resolver.go,确认 hasAllowlistOnlyAllowed 的 ANY 语义,再对比 v0.35.0 的 ALL 语义锁定版本修复。
  6. 验证「服务健康」的下游链路:绕过故障层直接查 k8s_gateway(<k8s-gateway-clusterip>:5353)证明该段本身正常,把问题收敛到单一层。
  7. 对可疑镜像/摘要做外部复核:直接 curl registry manifest,避免把 DNS 故障误判为镜像缺失。

常用排查命令:

# 解析矩阵
for n in <svc>.<home-domain> <home-domain> kubernetes.default.svc.cluster.local docker.io ad-assets.futurecdn.net; do
  printf "%-40s %s\n" "$n" "$(dig +short @<main-dns-ip> $n | tr '\n' ' ')"
done

# 拦截/分流原因
kubectl -n blocky logs deploy/blocky -c blocky --tail=200 | grep -oE "question_name=[^ ]+ .*response_reason=[^\"]*"

# 清 DNS 缓存(改配置后建议做一次)
kubectl -n blocky port-forward svc/blocky 14000:4000 &
curl -s -X POST http://127.0.0.1:14000/api/cache/flush

注意事项(已踩过的坑)

  1. 只配 allowlist 不配 denylist 的组 = 白名单组。在 blocky < v0.35.0 上会因「任一分组命中」而全局生效;务必保持 blocky ≥ v0.35.0(chart ≥ 22.23.1),或给该组补一个同名 denylist。
  2. mihomo 的 nameserver-policy 目标只写回环地址(本机限制,见修复 1);* 只匹配一级,需要 +.。
  3. k8s_gateway 的 fallthrough 不可配置:关掉 forward 就是明文 1.1.1.1:53,在带 TUN 的节点上会被劫持成 fake-ip 并被缓存下发,必须开启并指向不被劫持的 DoT/DoH。
  4. 改 CM 必须重启 pod(blocky 不触发文件热重载)。
  5. 集群相关域名写进 gfwlist/custom-domains.txt(domain|dnsserver 格式),否则会被列表刷新脚本覆盖。
  6. 配置只落在 release 里等于没有配置:rev14 的 blocking 块在磁盘上找不到出处,导致排障时先怀疑配置源。所有变更都要落 values.yaml。
  7. 某个镜像站域名目前仍 SERVFAIL,但 223.5.5.5、路由器、blocky 全都 SERVFAIL —— 是该域名上游本身不可解析,与本次配置无关。

回滚与文件清单

回滚:

# blocky:rev17 = 修复 1/2 之前(chart 22.25.0 已就位),rev16 = 事故回滚态
helm -n blocky rollback blocky 17

# mihomo:恢复改动前配置
cd ~/clustertool && cp mihomo/configmap.yaml.bak.20260922192151 mihomo/configmap.yaml \
  && kubectl apply -f mihomo/configmap.yaml && kubectl -n network rollout restart deploy/mihomo

本次涉及的文件与备份(~/clustertool/):

文件 说明
myapp/blocky/values.yaml 现行真源(含 blocking、k8sgateway.forward、≈7.2k 条 mapping)
myapp/blocky/values.yaml.bak.20260525202001 5 月的旧版本
myapp/blocky/values.yaml.bak.20260922185022 列表刷新前的状态(无 blocking)
myapp/blocky/values.yaml.bak.20260922185803 插入 blocking 之前
myapp/blocky/values.yaml.bak.fix12.20260922191920 修复 1/2 之前的 rev17 状态
gfwlist/custom-domains.txt 新增 cluster.local|<kube-dns-ip>:53
mihomo/configmap.yaml fake-ip-filter 用 +.;内网策略目标 → 127.0.0.53
mihomo/configmap.yaml.bak.20260922192151 mihomo 改动前状态

集群侧:release blocky rev18(chart 22.25.0 / blocky v0.35.0);kube-system/kube-dns ClusterIP <kube-dns-ip>;MetalLB LB 映射 <ingress-ip>(ingress-internal)/<ingress-external-ip>(ingress-external)/<main-dns-ip>(blocky)/<dashboard-ip>(dashboard)/<other-svc-ip>(其他自建服务)/<mihomo-service-ip>(mihomo)。

关联

  • k8s 部署 mihomo 问题记录:同一套 blocky + mihomo TUN + fake-ip 分流的部署与踩坑记录;本文是其上线运行后的第一次故障复盘与分流修复。